package com.cnkvha.trackcontestmgmt.web;

import com.cnkvha.trackcontestmgmt.TrackContestManagement;
import com.cnkvha.trackcontestmgmt.UserType;
import com.cnkvha.trackcontestmgmt.contest.Contest;
import com.cnkvha.trackcontestmgmt.contest.ContestState;
import com.cnkvha.trackcontestmgmt.contest.ContestTrackInfo;
import org.webbitserver.BaseWebSocketHandler;
import org.webbitserver.WebServer;
import org.webbitserver.WebSocketConnection;

import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Predicate;

public class TCMWebSocketHandler extends BaseWebSocketHandler {

    public static final String KEY_AUTHORIZED = "authorized";
    public static final String KEY_USER_TYPE = "user_type";
    public static final String KEY_USER_IDENTIFIER = "user_identifier";

    public static final String KEY_SELECTED_TRACK_CONTEST = "selected_track_contest_name";
    public static final String KEY_SELECTED_TRACK_INDEX = "selected_track_index";

    public static final String KEY_LAST_PING_RECEIVED = "last_ping_received";

    private final WebServer server;

    // used to check for duplicate sessions
    private Set<String> identifiers_logged_in = new HashSet<>();

    private Map<InetSocketAddress, WebSocketConnection> connections = Collections.synchronizedMap(new HashMap<>());

    public TCMWebSocketHandler(WebServer server) {
        this.server = server;
    }

    @Override
    public void onOpen(WebSocketConnection connection) {
        TrackContestManagement.instance.getLogger().log("新连接: " + connection.httpRequest().remoteAddress().toString());
        connections.put((InetSocketAddress) connection.httpRequest().remoteAddress(), connection);
        TrackContestManagement.instance.getLogger().log("连接数: " + connections.size());
        connection.send("auth;request");
        //connection.data(KEY_AUTHORIZED, false);
    }

    @Override
    public void onClose(WebSocketConnection connection) throws Exception {
        TrackContestManagement.instance.getLogger().log("连接断开: " + connection.httpRequest().remoteAddress().toString());
        connections.remove(connection.httpRequest().remoteAddress());
        TrackContestManagement.instance.getLogger().log("连接数: " + connections.size());

        if(connection.dataKeys().contains(KEY_AUTHORIZED) && (boolean)connection.data(KEY_AUTHORIZED) && connection.dataKeys().contains(KEY_USER_TYPE) && connection.dataKeys().contains(KEY_USER_IDENTIFIER)) {
            identifiers_logged_in.remove(connection.data(KEY_USER_IDENTIFIER));
        }

        if(connection.dataKeys().contains(KEY_AUTHORIZED) && (boolean)connection.data(KEY_AUTHORIZED) && connection.dataKeys().contains(KEY_USER_TYPE) && connection.dataKeys().contains(KEY_SELECTED_TRACK_CONTEST) && connection.dataKeys().contains(KEY_SELECTED_TRACK_INDEX)) {
            Contest contest = TrackContestManagement.instance.getCurrentContest();
            if(contest != null && contest.name.equals(connection.data(KEY_SELECTED_TRACK_CONTEST))) {
                UserType type = getUserType(connection);
                int track = (int) connection.data(KEY_SELECTED_TRACK_INDEX);
                if (type == UserType.TIMER) {
                    contest.tracks[track].count_timer --;
                    TrackContestManagement.log("[!] 有一名计时员离开了系统");
                }
            }
        }
    }

    @Override
    public void onMessage(WebSocketConnection connection, String msg) {
        // login
        if(msg.startsWith("auth;")) {
            String[] params = msg.split(";");
            if(params.length != 4) {
                connection.send("auth;fail;参数错误! ");
                connection.close();
                return;
            }
            UserType t;
            try {
                t = UserType.valueOf(params[1]);
            } catch (EnumConstantNotPresentException ex) {
                connection.send("auth;fail;err");
                connection.close();
                return;
            }
            if(t.getPassword() == null) {
                TrackContestManagement.log("[×] [%s] 登录失败 (%s) (密码未设置!)", connection.httpRequest().remoteAddress().toString(), t.getReadableName());
                connection.send("auth;fail;主控未设置密码! ");
                return;
            }
            String user_identifier = new String(Base64.getDecoder().decode(params[2]), StandardCharsets.UTF_8);
            String remote_pwd = new String(Base64.getDecoder().decode(params[3]), StandardCharsets.UTF_8);
            if(t.getPassword().equals(remote_pwd)) {
                if(identifiers_logged_in.contains(user_identifier)) {
                    connection.send("auth;fail;这个ID已经登陆了, 请勿重复登陆! ");
                    return;
                }
                if(!t.isAllowedIdentifier(user_identifier)) {
                    connection.send("auth;fail;这个ID不在允许的名单内, 无法登陆! ");
                    return;
                }
                if(t == UserType.BEGIN) {
                    if(countUserType(UserType.BEGIN) > 0) {
                        connection.send("auth;fail;发令员只能有一名! ");
                        return;
                    }
                }
                connection.data(KEY_USER_TYPE, t);
                connection.data(KEY_USER_IDENTIFIER, user_identifier);
                identifiers_logged_in.add(user_identifier);
                connection.send("auth;success");
                connection.data(KEY_AUTHORIZED, true);
                TrackContestManagement.log("[√] %s[%s] 成功登录为: %s", user_identifier, connection.httpRequest().remoteAddress().toString(), t.getReadableName());
            } else {
                TrackContestManagement.log("[×] %s[%s] 登录失败 (%s)", user_identifier, connection.httpRequest().remoteAddress().toString(), t.getReadableName());
                connection.send("auth;fail;密码错误! ");
            }
            return;
        }
        if(!connection.data().containsKey(KEY_AUTHORIZED) || !((boolean) connection.data(KEY_AUTHORIZED))) {
            connection.send("auth;fail;err");
            connection.close();
            return;
        }
        /* ==== SESSION ONLY ==== */
        long timestamp_message_received = System.currentTimeMillis(); // universal stuff
        // [ping] start
        if(msg.startsWith("ping;")) {
            if(connection.dataKeys().contains(KEY_LAST_PING_RECEIVED) && (System.currentTimeMillis() - (long)connection.data(KEY_LAST_PING_RECEIVED)) < 200 ) {
                TrackContestManagement.log("ping flood detected! ");
                connection.close();
                return;
            }
            connection.data(KEY_LAST_PING_RECEIVED, System.currentTimeMillis());
            String[] params = msg.substring(5).split(";");
            connection.send("pong;" + params[0]);
        }
        // [ping] end
        // [contest;get] start
        if(msg.startsWith("contest;get")) {
            Contest contest = TrackContestManagement.instance.getCurrentContest();
            if(contest == null || contest.state != ContestState.STANDBY) {
                connection.send("contest;state;no");
            } else {
                connection.send("contest;state;yes");
                connection.send("contest;data;" + contest.serializeIntoTCMData());
            }
            return;
        }
        // [contest;get] end
        // [track] start
        if(msg.startsWith("track;") && msg.length() > 6) {
            String select_track_name = msg.substring(6);
            UserType userType = getUserType(connection);
            Contest contest = TrackContestManagement.instance.getCurrentContest();
            if(contest == null) {
                connection.send("track;error;invalid_contest");
                return;
            }
            if(userType == UserType.TIMER) {
                if(connection.dataKeys().contains(KEY_SELECTED_TRACK_CONTEST) && connection.data(KEY_SELECTED_TRACK_CONTEST).equals(contest.name) && connection.dataKeys().contains(KEY_SELECTED_TRACK_INDEX)) {
                    // already selected
                    connection.send("track;error;already_selected");
                } else {
                    int index = contest.findIndexByName(select_track_name);
                    if(index == -1) {
                        connection.send("track;error;incorrect_name");
                    } else {
                        // selection save
                        connection.data(KEY_SELECTED_TRACK_CONTEST, contest.name);
                        connection.data(KEY_SELECTED_TRACK_INDEX, index);
                        contest.tracks[index].count_timer ++;
                        connection.send("track;confirmed");
                    }
                }
            } else {
                connection.send("track;error;incorrect_user_type");
            }
        }
        // [track] end
        // [control] start
        if(msg.startsWith("control;")) {
            Contest contest = TrackContestManagement.instance.getCurrentContest();
            String opt = msg.substring(8);
            if(opt.startsWith("begin;")) {
                long average_latency = Long.parseLong(opt.substring(6));
                if(getUserType(connection) == UserType.BEGIN) {
                    if(contest == null) return;
                    TrackContestManagement.instance.queueOnOperationThread(() -> {
                        // switch to TRACKING!
                        contest.start_time = System.currentTimeMillis() - average_latency;
                        contest.state = ContestState.TRACKING;
                        TrackContestManagement.instance.getWebSocketHandler().broadcastValidClients("state;" + contest.state.name());
                        TrackContestManagement.log("已经发令，开始计时！");
                    });
                }
            }
            return;
        }
        // [control] end
        // [timer;stop] start
        if(msg.startsWith("timer;stop;")) {
            // check prerequisites
            if(!connection.dataKeys().contains(KEY_USER_IDENTIFIER)) return;
            if(!connection.dataKeys().contains(KEY_USER_TYPE) || connection.data(KEY_USER_TYPE) != UserType.TIMER) return;
            Contest contest = TrackContestManagement.instance.getCurrentContest();
            if(contest == null) return;
            if(!connection.dataKeys().contains(KEY_SELECTED_TRACK_CONTEST) || !connection.data(KEY_SELECTED_TRACK_CONTEST).equals(contest.name) || !connection.dataKeys().contains(KEY_SELECTED_TRACK_INDEX)) return;
            int track_index = (int) connection.data(KEY_SELECTED_TRACK_INDEX);
            // run the algorithm
            ContestTrackInfo track = contest.tracks[track_index];
            String user_identifier = (String) connection.data(KEY_USER_IDENTIFIER);
            String[] params = msg.split(";");
            TrackContestManagement.instance.queueOnOperationThread(() -> {
                if(track.map_time_spent.containsKey(user_identifier)) {
                    connection.send("message;已经接收到停表信号, 无法重复发送！");
                    return;
                }
                try {
                    int avg_latency = Integer.parseInt(params[2]);
                    long time_spent = timestamp_message_received - contest.start_time - avg_latency;
                    track.map_time_spent.put(user_identifier, time_spent);
                    track.total_time_spent_for_avg_calc += time_spent;
                    track.received_timer_stop++;
                    track.avg_time_spent = track.total_time_spent_for_avg_calc / track.received_timer_stop;
                    if(track.realtime_order == -1) {
                        contest.realtime_order_index++;
                        track.realtime_order = contest.realtime_order_index;
                    }
                    TrackContestManagement.instance.getWebServer().getExecutor().execute(() -> connection.send("timer;confirmed;" + time_spent));
                    TrackContestManagement.log("接收到了 %s 的停表信号, 计时: %dms", user_identifier, time_spent);
                }catch (Exception e) {
                    TrackContestManagement.log(e.getClass().getSimpleName() + ": " + e.getMessage() + "\n" + e.getStackTrace()[0].toString());
                }

                // check contest state
                {
                    boolean all_captured = true;
                    for (ContestTrackInfo test_track : contest.tracks) {
                        if(test_track.received_timer_stop < test_track.count_timer) {
                            all_captured = false;
                            break;
                        }
                    }
                    if(all_captured) {
                        contest.state = ContestState.FINALIZING;
                        broadcastValidClients("state;" + contest.state.name());
                        TrackContestManagement.log("裁判已经全部发出结束信号, 比赛进入数据校正阶段! ");

                        // save up the data
                        TrackContestManagement.instance.getContestDataManager().save(contest);

                        // TODO: broadcast to rectifiers!
                        // ...
                    }
                }
            });
        }
        // [timer;stop] end
    }

    public void broadcastValidClients(String msg) {
        server.getExecutor().execute(() -> {
            connections.values().forEach(c -> {
                if(c.dataKeys().contains(KEY_AUTHORIZED) && ((boolean) c.data(KEY_AUTHORIZED))) {
                    c.send(msg);
                }
            });
        });
    }

    private UserType getUserType(WebSocketConnection connection) {
        if(!connection.dataKeys().contains(KEY_USER_TYPE)) return null;
        return (UserType) connection.data(KEY_USER_TYPE);
    }

    public void resetAllStates() {
        server.getExecutor().execute(() -> {
            connections.values().forEach(c -> {
                c.data().remove(KEY_SELECTED_TRACK_CONTEST);
                c.data().remove(KEY_SELECTED_TRACK_INDEX);
            });
        });
    }

    public int countUserType(UserType t) {
        return (int) connections.values().stream().filter(new Predicate<WebSocketConnection>() {
            @Override
            public boolean test(WebSocketConnection webSocketConnection) {
                if(!webSocketConnection.dataKeys().contains(KEY_AUTHORIZED) || !((boolean) webSocketConnection.data(KEY_AUTHORIZED)) || !webSocketConnection.dataKeys().contains(KEY_USER_TYPE)) return false;
                return webSocketConnection.data(KEY_USER_TYPE) == t;
            }
        }).count();
    }

}
